﻿using System;
using System.Linq;
using System.Reflection;
using OverStore.Runtime.Storage;
using OverStore.Runtime.PersistenceServiceProvider;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;

namespace OverStore.Runtime.ObjectRepository.Mapping
{
    /// <summary>
    /// <see cref="IMapper{TInstance}" /> implementation uses Reflection to obtain values from the properties of 
    /// TInstance object. The mapper does not support entity references and supports only properties which values 
    /// are primitive types. <see cref="ReflectionMapper{TInstance}"/> does not support object key obtaining.
    /// Use reflection mapper for map parameters for object readers.
    /// </summary>
    public sealed class ReflectionMapper<TInstance> : IMapper<TInstance>
        where TInstance : class
    {
        private IEnumerable<PropertyInfo> _Properties;

        /// <summary>
        /// Initializes new instance of the <see cref="ReflectionMapper{TInstance}" /> class.
        /// </summary>
        public ReflectionMapper()
        {
        }

        private IEnumerable<PropertyInfo> Properties
        {
            get
            {
                if (_Properties == null)
                    _Properties = typeof(TInstance)
                                    .GetProperties()
                                    .Where(p => p.CanRead && p.CanWrite)
                                    .Where(p => p.PropertyType == typeof(string) ||
                                                p.PropertyType == typeof(Guid) ||
                                                p.PropertyType.IsEnum ||
                                                IsNullableType(p.PropertyType) ||
                                                typeof(IConvertible).IsAssignableFrom(p.PropertyType))
                                    .ToList();
                return _Properties;
            }
        }

        private static bool IsNullableType(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            return type.IsGenericType &&
                   type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        /// <summary>
        /// Get data of object.
        /// Operation should not checks arguments for null etc for
        /// gain performance.
        /// </summary>
        /// <param name="instance">The instance to get data.</param>
        /// <param name="registry">The object allows to perform persistence operations over instances of different types.</param>
        /// <returns>The data of instance.</returns>
        /// <exception cref="PersistingRuntimeException">Error occured during mapping.</exception>
        public IRecord GetObjectData(TInstance instance, IRegistry registry)
        {
            return new ReflectionRecord(instance, this);
        }

        /// <summary>
        /// Apply data to object.
        /// Operation should not checks arguments for null etc for
        /// gain performance.
        /// </summary>
        /// <param name="data">The data for apply to result.</param>
        /// <param name="result">The result object for apply data to it.</param>
        /// <param name="registry">The object allows to perform persistence operations over instances of different types.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during mapping.</exception>
        public void ApplyObjectData(TypedRecord data, TInstance result, IRegistry registry)
        {
            try
            {
                foreach (var pi in Properties)
                {
                    object value;
                    if (!data.TryGetValue(pi.Name, out value))
                        pi.SetValue(result, value, null);
                }
            }
            catch (Exception ex)
            {
                throw new PersistingRuntimeException(
                    String.Format(
                        CultureInfo.InvariantCulture,
                        "Mapping error: applying object data to the instance of type {0} failed.",
                        typeof(TInstance)),
                    ex);
            }
        }

        private class ReflectionRecord : TypedRecordBase
        {
            private readonly TInstance _Instance;
            private readonly ReflectionMapper<TInstance> _Mapper;

            public ReflectionRecord(TInstance instance, ReflectionMapper<TInstance> mapper)
            {
                Debug.Assert(instance != null, "instance != null");
                Debug.Assert(mapper != null, "mapper != null");
                _Instance = instance;
                _Mapper = mapper;
            }

            public override bool TryGetValue(string name, out object result)
            {
                var pi = _Mapper.Properties.FirstOrDefault(p => StringComparer.OrdinalIgnoreCase.Equals(name, p.Name));
                if (pi != null)
                {
                    try
                    {
                        result = pi.GetValue(_Instance, null);
                        return true;
                    }
                    catch (Exception ex)
                    {
                        throw new PersistingRuntimeException(
                                String.Format(
                                    CultureInfo.InvariantCulture,
                                    "Mapping error: getting data of the instance of type {0} failed.",
                                    typeof(TInstance)),
                                ex);
                    }
                }
                else
                {
                    result = null;
                    return false;
                }
            }
        }
    }
}
